昨天講到封裝的一個重要觀念「保護層級」,筆者稱之為三個p's
(或P's
):p
ublic(公開), p
rotected(保護), 和p
rivate(私有)。今天將更深入說明。
先複習這三個p's
的意義:
public(公開):
- 完全開放,物件可在外部自由存取,封裝層級最低。
protected(保護):
- 僅物件及其子孫的內部才可存取,封裝層級中等。
private(私有):
- 只有類別(昨天誤作物件)自身內部方可存取,封裝層級最高。
上面說的「內部」和「外部」,請看以下程式(此編輯器的markdown,程式區塊好像沒有加行號功能):
class Tree():
def __init__(self, breed: str, age: int):
self.breed = breed
self.age = age
laozi = Tree('cedar', 2593) # 老子神木。
print('\n以下都算是「外部存取」:')
print('.透過object.attribute(物件.屬性)的表示法「取得」屬性。')
print(f" laozi's breed: {laozi.breed}") # 取值。
print('\n.透過object.attribute(物件.屬性)的表示法「修改」屬性。')
laozi.breed = 'phoebe' # 賦值。
print(f" laozi's breed: {laozi.breed}")
輸出:
其中laozi = Tree('cedar', 2593)
意思是以Tree()類別為藍本,建立一棵名為laozi(老子神木)的樹,樹種為cedar,樹齡2593歲。這棵神木就是一個物件(an object or an instance)。
print(f" laozi's breed: {laozi.breed}")
這行,就是上篇提到的「物件.屬性」表示式,以取得laozi物件的屬性breed的值。
laozi.breed = 'phoebe'
也很容易理解,很明顯是透過「物件.屬性」加上賦值,來修改laozi的屬性,將原來的'cedar'(雪松)改為'phoebe'(楠木)。
以上的「物件.屬性」表示方式,就是筆者說的「外部存取」。下面的截圖標明了何者是物件,何者是屬性:
別忘了之前所講,Python類別(物件)內的屬性,保護層級為「公開」(public,名稱無前綴底線)或「保護」(protected,屬性名稱有一條前綴底線)者,才可以透過「物件.屬性」表示式在外部存取。
再度提醒:Python類別中保護層級的屬性,只是「約定俗成」:大家有個默契,前綴一條底線的屬性就不要用「物件.屬性」的方式修改了。可這條前綴底線並無實質保護效果,類別使用者真要修改Python完全不加阻擋,有點防君子不防小人味道。筆者當然不是在暗示,修改了保護層級屬性,您就是「小人」。絕無此意。
有些人會將「前綴一條底線」的屬性也說成是「私有」而完全忽略「保護」這個層級。筆者認為這會造成混淆,所以還是採用三分法。不過由於Python的保護層級實際上沒有保護力,所以往後盡量不提這層,重點放在無前綴底線的「公開」和兩條(注意是兩條不是一條)底線的「私有」兩者。
至於「方法」,也和屬性一樣,只要是公開層級的方法,都可用「物件.方法」的表示式呼叫。當然,要呼叫方法,就和呼叫一般函數一樣,方法名稱後面要加小括號,例如應寫tree.get_breed()
而不是tree.get_breed
。
Python類別中的屬性,如果在其名稱的前面加兩條底線(註1),就自然變成「私有」(private)。這些屬性不能通過「物件.屬性」表示式存取:
class Tree():
def __init__(self, breed: str, age: int):
self.breed = breed # self.breed is a public attribute
self.__age = age # self.__age is a private attribute
laozi = Tree('cedar', 2593) # 建立物件。
print('\n.無法透過object.attribute表示法取得樹齡。')
print(laozi.__age) # 跑到這行時會出錯。
輸出:
物件欲存取私有屬性,唯有透過類別提供的公開方法。萬一類別沒有提供,就無法存取了:
class Tree():
def __init__(self, breed: str, age: int):
self.breed = breed # public attribute
self.__age = age # private attribute
def get_age(self) -> int: # public method
return self.__age
def set_age(self, age: int) -> None: # public method
self.__age = age
laozi = Tree('cedar', 2593) # 建立物件。
print('\n.呼叫類別內部提供的方法「取得」物件的private屬性。')
print(f' {laozi.get_age()=}') # 取值。
print('\n.呼叫類別內部提供的方法「修改」物件的private屬性。')
laozi.set_age('2704') # 賦值。
print(f' {laozi.get_age()=}')
輸出:
get_age()
和set_age()
這兩個公開的方法,是定義在類別之內,即是筆者所說的「內部存取」。這些類別內部方法,一方面可以存取私有屬性;另一方面,由於方法本身是公開層級,可以讓依該類別為模板而建立的物件,遵從「物件.方法」的表示式呼叫,從而存取私有屬性。
註1: 「前綴兩條底線的是私有屬性」的說法,其實不夠精準。Python私有屬性的正確定義是:「前綴雙底線而無後綴雙底線」。前後都有雙底線者是另一回事,至於是怎樣一回事,在此賣個關子,明天揭曉。